시각화

화재건수 시각화

코드
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

fire_df = pd.read_csv("./Raw Data/소방청_화재발생 정보.csv", encoding='cp949')

# 대구광역시 추출
cond1 = (fire_df['시도'] == '대구광역시')
daegu_fire_df = fire_df[cond1]
daegu_fire_df = daegu_fire_df[['화재발생년원일','시군구','화재유형','발화요인소분류','인명피해(명)소계','재산피해소계']]

daegu_fire_df['화재발생년원일'] = pd.to_datetime(daegu_fire_df['화재발생년원일'])


daegu_fire_by_type = daegu_fire_df.groupby('화재유형')[['시군구']].count().sort_values(by='시군구', ascending=False)
daegu_fire_by_type = daegu_fire_by_type.reset_index()
daegu_fire_by_type.rename(columns={'시군구': '화재건수'}, inplace=True)
daegu_fire_by_type


# 한글 폰트 설정 (윈도우 기준, mac은 'AppleGothic')
plt.rc('font', family='Malgun Gothic')

# 색상 지정: 건축,구조물만 진하게, 나머지는 연하게
color_map = ['#C62828' if x == '건축,구조물' else '#FFCDD2' for x in daegu_fire_by_type['화재유형']]

# 시각화
plt.figure(figsize=(10, 6))
sns.barplot(x='화재유형', y='화재건수', data=daegu_fire_by_type, palette=color_map)

plt.title('화재 유형별 대구화재 건수')
plt.xlabel('화재 유형')
plt.ylabel('화재 건수')
plt.xticks(rotation=0)

for i, v in enumerate(daegu_fire_by_type['화재건수']):
    try:
        val = float(v)
        plt.text(i, val + 50, str(v), ha='center', va='bottom', fontsize=12)
    except (ValueError, TypeError):
        # 숫자가 아니면 표시하지 않거나 0으로 처리
        pass

plt.tight_layout()
plt.show()


# 화재유형 건축,구조물 간추리고, 어디 구가 많은 화재가 일어나는지
daegu_building_fire_df = daegu_fire_df[daegu_fire_df['화재유형'] == '건축,구조물']
# daegu2
daegu_building_fire_by_gu = daegu_building_fire_df.groupby('시군구')[['화재유형']].count().sort_values(by='화재유형', ascending=False)
daegu_building_fire_by_gu = daegu_building_fire_by_gu.reset_index()
daegu_building_fire_by_gu.rename(columns={'화재유형': '화재건수'}, inplace=True)


daegu_population_df = pd.read_csv("./Data/동별인구.csv")
new_daegu_population_df = daegu_population_df[['군·구','등록인구 (명)','인구밀도 (명/㎢)','면적 (㎢)']]
new_by_gu = new_daegu_population_df.groupby('군·구').agg({
    '등록인구 (명)': 'sum',
    '인구밀도 (명/㎢)': 'mean',
    '면적 (㎢)': 'sum'
})
new_by_gu = new_by_gu.reset_index().rename(columns={'군·구': '시군구'})
new_by_gu

# 병합
merged = pd.merge(daegu_building_fire_by_gu,new_by_gu , how='left', on='시군구')
merged['화재건수/등록인구 (명)'] = merged['화재건수']/merged['등록인구 (명)'] * 100
merged['화재건수/인구밀도 (명/㎢)'] = merged['화재건수']/merged['인구밀도 (명/㎢)']
merged['화재건수/면적 (㎢)'] = merged['화재건수']/merged['면적 (㎢)']


merged =merged.sort_values(by='화재건수/인구밀도 (명/㎢)', ascending=False)
merged


#  인구밀도 대비 화재건수 발생 비율
si_list = merged['시군구']
values = merged['화재건수/인구밀도 (명/㎢)']

x = np.arange(len(si_list))
bar_width = 0.25

# 색상 지정: 군위군, 달성군 진하게, 나머지 연하게
colors = ['#AAAAAA'] * len(si_list)  # 모든 시군구 연한 회색
for idx, name in enumerate(si_list):
    if name in ['군위군', '달성군']:
        colors[idx] = '#1976D2'  # 진한 파란색 (원하는 색으로 변경 가능)

plt.figure(figsize=(14, 6))
bars = plt.bar(x, values, color=colors, width=bar_width, label='비율')

plt.xlabel('시군구')
plt.ylabel('화재건수/인구밀도 (명/㎢)')
plt.title('시군구별 인구밀도 대비 화재건수 비율 시각화')
plt.xticks(x, si_list, rotation=45)
plt.tight_layout()

# 막대 위에 숫자 표기
for bar in bars:
    height = bar.get_height()
    plt.annotate(f'{height:.3f}',
                 xy=(bar.get_x() + bar.get_width() / 2, height),
                 xytext=(0, 3), # 막대 위로 3pt 이동
                 textcoords="offset points",
                 ha='center', va='bottom', fontsize=10, color='black')

plt.show()

119안전센터 및 소방용수시설 위치 시각화

코드
# 대구광역시 119안전센터 및 소화장치 위치 시각화

# 데이터 출처
# 대구광역시_소방 긴급구조 비상 소화장치 현황
# https://www.data.go.kr/data/15117284/fileData.do

# 소방청_119안전센터 현황
# https://www.data.go.kr/data/15065056/fileData.do

import pandas as pd 
import numpy as np
import plotly.express as px
import plotly.graph_objects as go

loc_119 = pd.read_csv("./Data/대구광역시_소방서_위치.csv")
loc_fire = pd.read_csv("./Data/대구광역시_용수시설_위치.csv")




# 대구광역시 구별 소방 안전센터 시각화 
import json
with open ("./Data/시각화/대구_시군구_군위포함/대구_시군구_군위포함.geojson", encoding='utf-8') as f:
    geojson_data = json.load(f)
# print(geojson_data.keys())

import plotly.graph_objects as go

fig = go.Figure()
# 119안전센터(빨간점)
fig.add_trace(go.Scattermapbox(
    lat=loc_119["위도"],
    lon=loc_119["경도"],
    mode="markers",
    marker=go.scattermapbox.Marker(size=15, color="red"),
    name="119안전센터",  # 범례에 표시됨
    hovertemplate="<b>구:</b> %{customdata[0]}<br><b>동:</b> %{customdata[1]}<extra></extra>",
    customdata=loc_119[["구이름", "동이름"]].values,
))
fig.update_traces(marker=dict(size=15))

# 구별 소방 긴급구조 비상 소화장치 scatter mapbox
fig.add_trace(go.Scattermapbox(
    lat=loc_fire["위도"],
    lon=loc_fire["경도"],
    mode="markers",
    marker=go.scattermapbox.Marker(size=3, color="blue"),
    name="소화장치",
    hovertemplate="<b>구:</b> %{customdata[0]}<br><b>동:</b> %{customdata[1]}<extra></extra>",
    customdata=loc_fire["소재지지번주소"].values,
))

fig.update_layout(
    mapbox_style="carto-positron",
    mapbox_layers=[
        {
            "sourcetype": "geojson",
            "source": geojson_data,
            "type": "line",
            "color": "green",
            "line": {"width": 1},
        }
    ],
    mapbox_center={"lat": 35.8714, "lon": 128.6014},
    # zoom 값을 높이면 더 '줌인'됩니다. 지역에 따라 10~12 정도가 적당합니다.
    mapbox_zoom=11,
    margin={"r":0, "t":30, "l":0, "b":0},
)
fig.show()

소방서, 소방용수시설 거리 분포 시각화

코드
# %% 라이브러리 호출
import pandas as pd
import numpy as np
import plotly.express as px
# %% 데이터 로드
df = pd.read_csv('./Data/건축물대장_v0.5.csv')
hyd = pd.read_csv('./Data/대구광역시_용수시설_위치.csv')
#firestn = pd.read_csv('대구광역시_소방서_위치데이터.csv', encoding='cp949')
# %%
hydrant_lats = np.radians(hyd["위도"].values)
hydrant_lons = np.radians(hyd["경도"].values)
# %% 거리계산 함수 정의
def haversine_min_distance(lat1, lon1, hy_lats, hy_lons):
    R = 6371000  # 지구 반지름 (m)
    lat1 = np.radians(lat1)
    lon1 = np.radians(lon1)
    
    dlat = hy_lats - lat1
    dlon = hy_lons - lon1
    a = np.sin(dlat / 2)**2 + np.cos(lat1) * np.cos(hy_lats) * np.sin(dlon / 2)**2
    c = 2 * np.arcsin(np.sqrt(a))
    distances = R * c
    return distances.min()
# %% min({소화전거리(m)})
df['소방용수시설거리'] = df.apply(
    lambda row: haversine_min_distance(row["위도"], row["경도"], hydrant_lats, hydrant_lons),
    axis=1
)
# %%
df['소방용수시설거리'].head()
# %% 소방서 데이터
firestation = pd.read_csv('./Data/대구광역시_소방서_위치.csv')
firestation.head()
# %% min({소방서거리(m)})
station_lats = np.radians(firestation["위도"].values)
station_lons = np.radians(firestation["경도"].values)
df["소방서거리"] = df.apply(
    lambda row: haversine_min_distance(row["위도"], row["경도"], station_lats, station_lons),
    axis=1
)

# %% 소방서거리, 소화전거리 분포 시각화

# 소방서거리 분포
fig1 = px.histogram(df, x="소방서거리", nbins=100, title="가장 가까운 소방서 거리 분포", marginal="box")
fig1.update_layout(
    bargap=0.1,
    xaxis_title="거리(m)",
    yaxis_title="건물 수",
    template='plotly_white'
)

fig1.show()

# 소화전거리 분포
fig2 = px.histogram(df, x="소방용수시설거리", nbins=100, title="가장 가까운 소방용수시설 거리 분포", marginal="box")
fig2.update_layout(
    bargap=0.1,
    xaxis_title="거리(m)",
    yaxis_title="건물 수",
    template='plotly_white'
)

fig2.show()

노령 인구 시각화

코드
#======================================
# 노령 인구 비율 시각화
#======================================

# 동별 노령인구 비율 시각화
import pandas as pd 
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
df = pd.read_csv("./Data/동별인구.csv")
new = df[['군·구', '동·읍·면', '고령자_비율','위도','경도']]

# 동별 고령자 비율 값
g2_by_dong = new.groupby(['동·읍·면'])[['고령자_비율']].sum()
g2_by_dong = g2_by_dong.sort_values(by='고령자_비율',ascending=False)
g2_by_dong.rename(columns={'고령자_비율': '고령자_평균비율'}, inplace=True)
g2_by_dong = g2_by_dong.reset_index()
# g2_by_dong.info()

import geopandas as gpd
gdf = gpd.read_file("./Data/시각화/대구_행정동/대구_행정동_군위포함.shp")
print(gdf.crs)
gdf = gdf.to_crs(epsg=4326)
# gdf.to_file("./Data/대구_행정동_군위포함.geojson", driver="GeoJSON")

import json
with open("./Data/시각화/대구_행정동/대구_행정동_군위포함.geojson", encoding='utf-8') as f:
 geojson_data = json.load(f)
# print(geojson_data.keys())
# print(geojson_data['features'][0]['properties'])

# gdf 파일에 유천동이 없고 g2_by_dong 파일에 유천동이 있어 행 삭제
cond = (gdf['ADM_DR_CD'] == '유천동')
gdf[cond]
g2_by_dong.rename(columns={'동·읍·면': 'ADM_DR_NM'}, inplace=True)
cond = (g2_by_dong['ADM_DR_NM'] == '유천동')
g2_by_dong = g2_by_dong.drop(g2_by_dong[cond].index)

# 불로봉무동 이름 변경
g2_by_dong.loc[g2_by_dong['ADM_DR_NM'] == '불로봉무동', 'ADM_DR_NM'] = '불로·봉무동'


# 동별 노령인구 비율 시각화
fig = px.choropleth_mapbox(g2_by_dong,
 geojson=geojson_data,
 locations="ADM_DR_NM",
 featureidkey="properties.ADM_DR_NM",
 color="고령자_평균비율",
 color_continuous_scale="Greens",
 mapbox_style="carto-positron",
 center={"lat":35.87702415809577, "lon":128.58970500739858},
 zoom=10,                
opacity=0.7,               
title="대구광역시 동별 노인평균인구비율"  
)
fig.update_layout(margin={"r":0,"t":30,"l":0,"b":0}) 
fig.show() 


# ===================================
# 구별 고령자 비율 평균
g1_by_gu = new.groupby(['군·구'])[['고령자_비율']].mean()
g1_by_gu = g1_by_gu.reset_index()
g1_by_gu = g1_by_gu.sort_values(by='고령자_비율',ascending=False)
g1_by_gu.rename(columns={'군·구': 'SIGUNGU_NM', '고령자_비율': '고령자_평균비율',}, inplace=True)


import geopandas as gpd
gdf2 = gpd.read_file("./Data/시각화/대구_시군구_군위포함/대구광역시_시군구_군위포함.shp")
print(gdf2.crs)
gdf2 = gdf2.to_crs(epsg=4326)
# gdf2.to_file("./Data/대구_시군구_군위포함.geojson", driver="GeoJSON")

import json
with open("./Data/시각화/대구_시군구_군위포함/대구_시군구_군위포함.geojson", encoding='utf-8') as f:
 geojson_data2 = json.load(f)
print(geojson_data2.keys())

print(geojson_data2['features'][0]['properties'])

# 구별 노령 인구 비율 시각화
fig = px.choropleth_mapbox(g1_by_gu,
 geojson=geojson_data2,
 locations="SIGUNGU_NM",
 featureidkey="properties.SIGUNGU_NM",
 color="고령자_평균비율",
 color_continuous_scale="Greens",
 mapbox_style="carto-positron",
 center={"lat":35.87702415809577, "lon":128.58970500739858},
 zoom=10,                
opacity=0.7,               
title="대구광역시 구별 노인평균인구비율"  
)
fig.update_layout(margin={"r":0,"t":30,"l":0,"b":0}) 
fig.show()
PROJCS["Korea_2000_Korea_Unified_Coordinate_System",GEOGCS["GCS_Korea_2000",DATUM["Korean_Geodetic_Datum_2002",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6737"]],PRIMEM["Greenwich",0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",38],PARAMETER["central_meridian",127.5],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",1000000],PARAMETER["false_northing",2000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH]]
PROJCS["Korea_2000_Korea_Unified_Coordinate_System",GEOGCS["GCS_Korea_2000",DATUM["Korean_Geodetic_Datum_2002",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6737"]],PRIMEM["Greenwich",0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",38],PARAMETER["central_meridian",127.5],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",1000000],PARAMETER["false_northing",2000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH]]
dict_keys(['type', 'name', 'crs', 'features'])
{'BASE_DATE': '20210630', 'SIGUNGU_CD': '22010', 'SIGUNGU_NM': '중구', 'sgg_code': 22010.0, 'sggcd': 22010}

건축물대장 시각화

코드
# %% 라이브러리 호출
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
# %% check
# columns_to_check = ['Column14', 'Column15', 'Column60', 'Column61', 'Column67']
# %% 구/군 별 데이터 로드
df1 = pd.read_csv('./Raw Data/건축물대장/건축물대장_대구광역시_군위군.csv')
df2 = pd.read_csv('./Raw Data/건축물대장/건축물대장_대구광역시_남구.csv')
df3 = pd.read_csv('./Raw Data/건축물대장/건축물대장_대구광역시_달서구.csv')
df4 = pd.read_csv('./Raw Data/건축물대장/건축물대장_대구광역시_달성군.csv')
df5 = pd.read_csv('./Raw Data/건축물대장/건축물대장_대구광역시_동구.csv')
df6 = pd.read_csv('./Raw Data/건축물대장/건축물대장_대구광역시_북구.csv')
df7 = pd.read_csv('./Raw Data/건축물대장/건축물대장_대구광역시_서구.csv')
df8 = pd.read_csv('./Raw Data/건축물대장/건축물대장_대구광역시_수성구.csv')
df9 = pd.read_csv('./Raw Data/건축물대장/건축물대장_대구광역시_중구.csv')

# %% 구/군 컬럼 추가
df1['군/구'] = '군위군'
df2['군/구'] = '남구'
df3['군/구'] = '달서구'
df4['군/구'] = '달성군'
df5['군/구'] = '동구'
df6['군/구'] = '북구'
df7['군/구'] = '서구'
df8['군/구'] = '수성구'
df9['군/구'] = '중구'
# %% 구/군 별 데이터 통합
df_all = pd.concat([df1, df2, df3, df4, df5, df6, df7, df8, df9], ignore_index=True)
# %% 구조 분류 딕셔너리 정의
structure_map = {
    '목조 계열': ['일반목구조', '목구조', '트러스목구조', '통나무구조'],
    '조적식 구조': ['석구조', '벽돌구조', '블록구조', '시멘트블럭조', '흙벽돌조', '조적구조', '기타조적구조'],
    '콘크리트 계열': ['철근콘크리트구조','콘크리트구조','프리케스트콘크리트구조','보강콘크리트조','기타콘크리트구조','라멘조'],
    '철골 계열': ['일반철골구조','경량철골구조','강파이프구조','철파이프조','기타강구조','스틸하우스조','단일형강구조','철골구조','공업화박판강구조(PEB)','트러스구조',
    '철골콘크리트구조','철골철근콘크리트구조','철골철근콘크리트합성구조','기타철골철근콘크리트구조'],
    '조립식·판넬·기타': ['조립식판넬조', '컨테이너조'],
    '기타 / 특수 구조': ['막구조', '기타구조']
}
# %% 구조 분류
def map_structure_type(name):
    for group, items in structure_map.items():
        if name in items:
            return group
    return '미분류'
df_all['구조그룹'] = df_all['구조코드명'].apply(map_structure_type)
# %% 건축 자재별 분포 시각화
structure_counts = df_all['구조그룹'].value_counts()

# 도넛차트 그리기
fig = go.Figure(data=[go.Pie(
    labels=structure_counts.index,
    values=structure_counts.values,
    hole=0.4,
    textinfo='percent+label',
    hoverinfo='label+value+percent',
    insidetextorientation='radial'
)])

fig.update_layout(
    title_text='건축 자재별 건물 분포',
    annotations=[dict(text='', x=0.5, y=0.5, font_size=18, showarrow=False)],
    showlegend=True
)

fig.show()
# %% 주용도 분류 딕셔너리 정의
building_use = {
    '숙박/다중이용시설': ['숙박시설', '야영장시설', '관광휴게시설'],
    '공장/창고시설': ['공장','창고시설'],
    '교육/복지/의료/수련': ['노유자시설', '교육연구시설', '교육연구및복지시설', '의료시설', '수련시설'],
    '상업/판매/문화/업무/근린/생활편익':
    ['제2종근린생활시설',
    '근린생활시설',
    '제1종근린생활시설',
    '종교시설',
    '문화및집회시설',
    '운동시설',
    '업무시설',
    '판매시설',
    '위락시설',
    '판매및영업시설',
    '기타제1종근린생활시설',
    '생활편익시설',
    '소매점'],
    '기반시설':
    ['동물및식물관련시설',
    '위험물저장및처리시설',
    '자원순환관련시설',
    '분뇨.쓰레기처리시설',
    '방송통신시설',
    '자동차관련시설',
    '장례시설',
    '운수시설',
    '교정및군사시설',
    '국방,군사시설',
    '발전시설',
    '묘지관련시설'],
    '주거':
    ['단독주택',
    '공동주택',
    '다가구주택'],
    '행정/공공':
    '공공용시설',
}
# %% 주용도 분류
def use_type(name):
    if not isinstance(name, str):
        return '미분류'

    for group, items in building_use.items():
        if name in items:
            return group
    return '미분류'
df_all['주용도그룹'] = df_all['주용도코드명'].apply(use_type)
# %% 용도별 분포 시각화
use_group_counts = df_all['주용도그룹'].value_counts()
use_group_ratio = use_group_counts / use_group_counts.sum()

# 2% 미만은 기타로 묶기
threshold = 0.02
labels = []
values = []
etc_total = 0

for label, ratio in use_group_ratio.items():
    if ratio >= threshold:
        labels.append(label)
        values.append(use_group_counts[label])
    else:
        etc_total += use_group_counts[label]

# 기타 항목 추가
if etc_total > 0:
    labels.append('기타')
    values.append(etc_total)

# 도넛 차트 생성
fig1 = go.Figure(data=[go.Pie(
    labels=labels,
    values=values,
    hole=0.3,  # 도넛 중앙 구멍 작게 = 도넛 자체 크게
    textinfo='percent+label',
    hoverinfo='label+value+percent',
    insidetextorientation='radial'
)])

# 레이아웃 조정
fig1.update_layout(
    title_text='주용도그룹 분포 (2% 미만 기타로 통합)',
    annotations=[dict(text='주용도', x=0.5, y=0.5, font_size=20, showarrow=False)],
    showlegend=True,
    height=600,  # 높이 늘려서 크게 보기
    width=700
)

fig1.show()
# %% 용도, 자재 교차 분석 시각화
cross_tab = pd.crosstab(df_all['주용도그룹'], df_all['구조그룹'])
fig2 = go.Figure()
for 구조 in cross_tab.columns:
    fig2.add_trace(go.Bar(
        x=cross_tab.index,
        y=cross_tab[구조],
        name=구조
    ))

# 레이아웃 설정
fig2.update_layout(
    barmode='stack',  # 스택형 막대
    title='주용도그룹 vs 구조그룹 분포 (스택형 막대 그래프)',
    xaxis_title='주용도그룹',
    yaxis_title='건물 수',
    legend_title='구조그룹',
    template='plotly_white'
)

fig2.show()
# %% 비상용 승강기 수 분포 시각화
cond_elevator = df_all['지상층수'] >= 5
emergency = df_all[cond_elevator]
# 결측치 0으로 대치
emergency['비상용승강기수'] = emergency['비상용승강기수'].fillna(0).astype(int)

# 5개 이상은 '5개 이상'으로 범주화
def categorize_elevators(x):
    return str(x) if x < 5 else '5개 이상'

emergency['비상용승강기_그룹'] = emergency['비상용승강기수'].apply(categorize_elevators)

# 그룹별 건물 수 집계
grouped = emergency['비상용승강기_그룹'].value_counts().sort_index().reset_index()
grouped.columns = ['비상용승강기수', '건물수']

# 파이차트 시각화 (파이 크기 크게 설정)
fig = px.pie(grouped,
             names='비상용승강기수',
             values='건물수',
             title='지상 5층 이상 건물의 비상용 승강기 수 분포',
             width=700, height=700,  # 파이 크기 조절
             color_discrete_sequence=px.colors.sequential.Magma)

# 퍼센트와 라벨 모두 표시
fig.update_traces(textinfo='percent+label',
                  textfont_size=16,
                  pull=[0.03]*len(grouped))  # 조각 약간 분리(선택)

fig.show()
# %% 사용승인일 이상값 탐색(보충 필요)
df_all['사용승인일_길이'] = df_all['사용승인일'].astype(str).str.len()
df_all['사용승인일_길이'].unique()
cond = df_all['사용승인일_길이'] == 9
df_all[cond]['사용승인일'].unique()
df_year = df_all.copy()
cond_y9 = df_year['사용승인일_길이'] == 9
df_year.loc[cond_y9, '사용승인일'] = '19' + df_year.loc[cond_y9, '사용승인일'].astype(str)
cond_y11 = df_year['사용승인일'] == '191979100.0'
df_year[cond_y11]
df_year.loc[cond_y11, '사용승인일'] = df_year.loc[cond_y11, '사용승인일'].str[2:]
df_year['사용승인일_길이'] = df_year['사용승인일'].astype(str).str.len()
cond_drop = df_year['사용승인일_길이'].isin([2, 3, 5])
df_year = df_year[~cond_drop]
df_year.loc[:, '사용승인일'] = df_year['사용승인일'].astype(str).str.strip()
# %% 사용승인일(년도) 추출
df_year.loc[:, '사용승인일(년도)'] = df_year['사용승인일'].astype(str).str[:4]
df_year['사용승인일(년도)'] = df_year['사용승인일(년도)'].astype(str).str.strip()
df_year['사용승인일(년도)'].replace('', pd.NA, inplace=True)
df_year['사용승인일(년도)'] = pd.to_numeric(df_year['사용승인일(년도)'], errors='coerce').astype('Int64')
# %% 승인연도 필터링, 연령 계산 
cleaned_year = df_year.dropna(subset='사용승인일(년도)')
filltered_year = cleaned_year[cleaned_year['사용승인일(년도)'] >= 1800]
filltered_year['연령'] = 2025 - filltered_year['사용승인일(년도)']
# %% 건축물 연령 분포 시각화
bins = list(range(0, 101, 10)) + [float('inf')]
labels = [f"{i}~{i+10}년" for i in range(0, 100, 10)] + ["100년 이상"]

filltered_year['연령대'] = pd.cut(filltered_year['연령'], bins=bins, labels=labels, right=False)
# 연령대별 건물 수 집계
age_group_counts = filltered_year['연령대'].value_counts().sort_index()
# Plotly로 막대 그래프 시각화

fig5 = px.bar(
    x=age_group_counts.index,
    y=age_group_counts.values,
    labels={'x': '연령대', 'y': '건물 수'},
    title='노후화 구간별 건물 수 분포 (10년 단위)',
    text=age_group_counts.values,
    color=age_group_counts.values,
    color_continuous_scale='Viridis'
)

fig5.update_layout(
    xaxis_title="노후화 구간",
    yaxis_title="건물 수",
    uniformtext_minsize=8,
    uniformtext_mode='hide',
    bargap=0.3
)

fig5.show()
# %% 40년 이상은 한 범주로 처리한 것
bins = [0, 10, 20, 30, 40, float('inf')]
labels = ['0~10년', '10~20년', '20~30년', '30~40년', '40년 이상']

# 2. 구간화
filltered_year['연령대'] = pd.cut(filltered_year['연령'], bins=bins, labels=labels, right=False)

# 3. 집계
age_group_counts = filltered_year['연령대'].value_counts(sort=False)

# 4. 시각화
fig6 = px.bar(
    x=age_group_counts.index,
    y=age_group_counts.values,
    labels={'x': '연령대', 'y': '건물 수'},
    title='노후화 구간별 건물 수 분포 (40년 이상 묶음)',
    text=age_group_counts.values,
    color=age_group_counts.values,
    color_continuous_scale='Viridis'
)

fig6.update_layout(
    xaxis_title="노후화 구간",
    yaxis_title="건물 수",
    uniformtext_minsize=8,
    uniformtext_mode='hide',
    bargap=0.3
)

fig6.show()
# %% 용도, 노후화 교차
bins = [0, 10, 20, 30, 40, float('inf')]
labels = ['0~10년', '10~20년', '20~30년', '30~40년', '40년 이상']
filltered_year['연령대'] = pd.cut(filltered_year['연령'], bins=bins, labels=labels, right=False)

# 2. 교차표 생성: 주용도그룹 × 연령대
# cross_tab = pd.crosstab(filltered_year['주용도그룹'], filltered_year['연령대'])
cross_tab = pd.crosstab(filltered_year['주용도그룹'], filltered_year['연령대'])

# 0인 값이 모두 들어있는 열(연령대) 제거
cross_tab = cross_tab.loc[:, (cross_tab != 0).any(axis=0)]

# 0인 값이 모두 들어있는 행(주용도그룹) 제거
cross_tab = cross_tab[(cross_tab != 0).any(axis=1)]

# 3. Plotly로 교차 막대그래프 (그룹별 스택)
fig7 = px.bar(
    cross_tab,
    x=cross_tab.index,
    y=cross_tab.columns,
    labels={'value': '건물 수', '주용도그룹': '주용도 그룹', '연령대': '연령대'},
    title='주용도 그룹별 연령대별 건물 수',
    barmode='stack'  # 누적 막대
)

fig7.update_layout(
    xaxis_title='주용도 그룹',
    yaxis_title='건물 수',
    legend_title='연령대',
    bargap=0.2
)

fig7.show()
# %%

노령 인구와 건물 노후화 상관관계

코드
import pandas as pd
import numpy as np
import plotly.express as px
from scipy.stats import pearsonr, spearmanr

# 데이터 불러오기
df_population = pd.read_csv("./Data/동별인구.csv")
df_population_filter = df_population[['군·구', '동·읍·면', '고령자_비율','위도','경도']]

df_building = pd.read_csv("./Data/건축물대장_v0.5.csv")

# ==============================
# 2) 동별 고령자 평균비율
# ==============================
df_pop = (
    df_population_filter
    .groupby(['군·구','동·읍·면'], as_index=False)['고령자_비율'].mean()
    .rename(columns={'고령자_비율':'고령자_평균비율'})
)

# 행정동명 표준화(예외 처리)
df_pop['행정동명'] = df_pop['동·읍·면'].replace({'불로봉무동':'불로·봉무동'})

# ==============================
# 3) 행정동별 평균 건물 노후도 점수
# ==============================
df_bld = (
    df_building
    .groupby('ADM_DR_NM', as_index=False)['건물노후도점수'].mean()
    .rename(columns={'건물노후도점수':'평균건물노후도점수'})
)

# ==============================
# 4) 병합
# ==============================
df_m = (
    df_pop
    .assign(ADM_DR_NM=df_pop['행정동명'])
    .merge(df_bld, on='ADM_DR_NM', how='inner')
)

# 고령자 비율 퍼센트로 변환
if df_m['고령자_평균비율'].max() <= 1.0:
    df_m['고령자_평균비율(%)'] = df_m['고령자_평균비율'] * 100
else:
    df_m['고령자_평균비율(%)'] = df_m['고령자_평균비율']

# ==============================
# 5) 구/군별 상관계수 계산 함수
# ==============================
def safe_corr(x, y, method='pearson'):
    x = pd.Series(x).astype(float)
    y = pd.Series(y).astype(float)
    mask = x.notna() & y.notna() & np.isfinite(x) & np.isfinite(y)
    x, y = x[mask], y[mask]
    if len(x) < 3 or x.nunique() < 2 or y.nunique() < 2:
        return np.nan, np.nan, len(x)
    if method == 'pearson':
        r, p = pearsonr(x, y)
    else:
        r, p = spearmanr(x, y)
    return r, p, len(x)

rows = []
for gugu, g in df_m.groupby('군·구', dropna=False):
    r_p, p_p, n_p = safe_corr(g['평균건물노후도점수'], g['고령자_평균비율(%)'], 'pearson')
    r_s, p_s, n_s = safe_corr(g['평균건물노후도점수'], g['고령자_평균비율(%)'], 'spearman')
    rows.append({
        '군·구': gugu,
        'n': int(n_p),
        'pearson_r': r_p,
        'pearson_p': p_p,
        'spearman_rho': r_s,
        'spearman_p': p_s
    })

corr_df = pd.DataFrame(rows)

# ==============================
# 6) 피어슨 상관계수 시각화
# ==============================
corr_df_plot = corr_df.sort_values('pearson_r', na_position='last')

fig_corr_p = px.bar(
    corr_df_plot,
    x='pearson_r',
    y='군·구',
    orientation='h',
    color='pearson_r',
    color_continuous_scale='RdBu',
    range_color=(-1, 1),
    labels={'pearson_r': '피어슨 상관계수 r', '군·구': '구/군'},
    title='구/군별 상관계수 (피어슨) — 고령인구 비율 vs 평균 건물 노후도 점수'
)
fig_corr_p.add_vline(x=0, line_width=1, line_dash='dash', line_color='gray')
fig_corr_p.update_layout(margin=dict(l=80, r=30, t=60, b=40))
fig_corr_p.show()

# ==============================
# 7) 스피어만 상관계수 시각화
# ==============================
corr_df_plot_s = corr_df.sort_values('spearman_rho', na_position='last')

fig_corr_s = px.bar(
    corr_df_plot_s,
    x='spearman_rho',
    y='군·구',
    orientation='h',
    color='spearman_rho',
    color_continuous_scale='RdBu',
    range_color=(-1, 1),
    labels={'spearman_rho': '스피어만 순위상관 ρ', '군·구': '구/군'},
    title='구/군별 상관계수 (스피어만) — 고령인구 비율 vs 평균 건물 노후도 점수'
)
fig_corr_s.add_vline(x=0, line_width=1, line_dash='dash', line_color='gray')
fig_corr_s.update_layout(margin=dict(l=80, r=30, t=60, b=40))
fig_corr_s.show()

# ==============================
# 8) 요약 테이블 출력
# ==============================
print(corr_df[['군·구','n','pearson_r','pearson_p','spearman_rho','spearman_p']].round(4).to_string(index=False))
군·구  n  pearson_r  pearson_p  spearman_rho  spearman_p
군위군  8     0.2105     0.6169        0.4524      0.2604
 남구 13     0.8124     0.0007        0.7802      0.0017
달서구 22     0.5901     0.0038        0.5810      0.0046
달성군  9     0.6582     0.0539        0.8167      0.0072
 동구 22     0.6210     0.0020        0.4771      0.0247
 북구 23     0.6310     0.0012        0.6126      0.0019
 서구 17     0.3474     0.1719        0.4118      0.1005
수성구 23     0.2488     0.2523        0.2628      0.2256
 중구 12     0.0895     0.7820       -0.0210      0.9484

노령 인구와 건물 수 상관관계

코드
import pandas as pd
import numpy as np
import plotly.express as px
from scipy.stats import pearsonr, spearmanr

# 데이터 불러오기
df_population = pd.read_csv("./Data/동별인구.csv")
df_population_filter = df_population[['군·구', '동·읍·면', '고령자_비율','위도','경도']]

df_building = pd.read_csv("./Data/건축물대장_v0.5.csv")

# ==============================
# 2) 동별 고령자 평균비율
# ==============================
df_pop = (
    df_population_filter
    .groupby(['군·구','동·읍·면'], as_index=False)['고령자_비율'].mean()
    .rename(columns={'고령자_비율':'고령자_평균비율'})
)

# 행정동명 표준화(예외 처리)
df_pop['행정동명'] = df_pop['동·읍·면'].replace({'불로봉무동':'불로·봉무동'})

# ==============================
# 3) 행정동별 건물수
# ==============================
df_bld = (
    df_building
    .groupby('ADM_DR_NM', as_index=False)['건물노후도점수'].size()
    .rename(columns={'size':'건물수'})
)

# ==============================
# 4) 병합
# ==============================
df_m = (
    df_pop
    .assign(ADM_DR_NM=df_pop['행정동명'])
    .merge(df_bld, on='ADM_DR_NM', how='inner')
)

# 고령자 비율 퍼센트로 변환
if df_m['고령자_평균비율'].max() <= 1.0:
    df_m['고령자_평균비율(%)'] = df_m['고령자_평균비율'] * 100
else:
    df_m['고령자_평균비율(%)'] = df_m['고령자_평균비율']

# ==============================
# 5) 구/군별 상관계수 계산 함수
# ==============================
def safe_corr(x, y, method='pearson'):
    x = pd.Series(x).astype(float)
    y = pd.Series(y).astype(float)
    mask = x.notna() & y.notna() & np.isfinite(x) & np.isfinite(y)
    x, y = x[mask], y[mask]
    if len(x) < 3 or x.nunique() < 2 or y.nunique() < 2:
        return np.nan, np.nan, len(x)
    if method == 'pearson':
        r, p = pearsonr(x, y)
    else:
        r, p = spearmanr(x, y)
    return r, p, len(x)

rows = []
for gugu, g in df_m.groupby('군·구', dropna=False):
    r_p, p_p, n_p = safe_corr(g['건물수'], g['고령자_평균비율(%)'], 'pearson')
    r_s, p_s, n_s = safe_corr(g['건물수'], g['고령자_평균비율(%)'], 'spearman')
    rows.append({
        '군·구': gugu,
        'n': int(n_p),
        'pearson_r': r_p,
        'pearson_p': p_p,
        'spearman_rho': r_s,
        'spearman_p': p_s
    })

corr_df = pd.DataFrame(rows)

# ==============================
# 6) 피어슨 상관계수 시각화
# ==============================
corr_df_plot = corr_df.sort_values('pearson_r', na_position='last')

fig_corr_p = px.bar(
    corr_df_plot,
    x='pearson_r',
    y='군·구',
    orientation='h',
    color='pearson_r',
    color_continuous_scale='RdBu',
    range_color=(-1, 1),
    labels={'pearson_r': '피어슨 상관계수 r', '군·구': '구/군'},
    title='구/군별 상관계수 (피어슨) — 고령인구 비율 vs 건물수'
)
fig_corr_p.add_vline(x=0, line_width=1, line_dash='dash', line_color='gray')
fig_corr_p.update_layout(margin=dict(l=80, r=30, t=60, b=40))
fig_corr_p.show()

# ==============================
# 7) 스피어만 상관계수 시각화
# ==============================
corr_df_plot_s = corr_df.sort_values('spearman_rho', na_position='last')

fig_corr_s = px.bar(
    corr_df_plot_s,
    x='spearman_rho',
    y='군·구',
    orientation='h',
    color='spearman_rho',
    color_continuous_scale='RdBu',
    range_color=(-1, 1),
    labels={'spearman_rho': '스피어만 순위상관 ρ', '군·구': '구/군'},
    title='구/군별 상관계수 (스피어만) — 고령인구 비율 vs 건물수'
)
fig_corr_s.add_vline(x=0, line_width=1, line_dash='dash', line_color='gray')
fig_corr_s.update_layout(margin=dict(l=80, r=30, t=60, b=40))
fig_corr_s.show()

# ==============================
# 8) 요약 테이블 출력
# ==============================
print(corr_df[['군·구','n','pearson_r','pearson_p','spearman_rho','spearman_p']].round(4).to_string(index=False))
군·구  n  pearson_r  pearson_p  spearman_rho  spearman_p
군위군  8    -0.9345     0.0007       -0.9286      0.0009
 남구 13     0.3053     0.3104        0.2912      0.3344
달서구 22     0.0091     0.9678        0.0728      0.7473
달성군  9    -0.2051     0.5965        0.0000      1.0000
 동구 22     0.3352     0.1272        0.2964      0.1804
 북구 23     0.3252     0.1300        0.2964      0.1696
 서구 17     0.3730     0.1403        0.2917      0.2560
수성구 23    -0.0179     0.9353        0.2026      0.3539
 중구 12    -0.0397     0.9024       -0.1958      0.5419

종합점수 분포

코드
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

df = pd.read_csv('./Data/건축물대장_v0.5.csv')

plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False

# 종합점수 시각화
plt.figure(figsize=(10, 6))
sns.histplot(df["종합점수"], kde=False, bins=50, color="skyblue")
plt.title("종합점수 분포", fontsize=16)
plt.xlabel("종합점수", fontsize=12)
plt.ylabel("건물 수", fontsize=12)
plt.show()

동별 종합점수 q1q3 바깥값 시각화

코드
# -*- coding: utf-8 -*-
import csv, json, re
import numpy as np
import pandas as pd
import plotly.express as px
from pathlib import Path

# ================== 경로 ==================
csv_path = Path("./Data/건축물대장_v0.5.csv")
geojson_path = Path("./Data/시각화/대구_행정동/대구_행정동_군위포함.geojson")
# ========================================

# ========== 1) CSV 로드 (구분자 자동 감지) ==========
with open(csv_path, "r", encoding="utf-8", errors="ignore") as f:
    sample = "".join([next(f) for _ in range(50)])
dialect = csv.Sniffer().sniff(sample, delimiters=[",", "\t", ";", "|"])
sep = dialect.delimiter

df = pd.read_csv(csv_path, sep=sep, engine="python", encoding="utf-8")

with open(geojson_path, "r", encoding="utf-8") as f:
    gj = json.load(f)

# 점수 숫자화
score_col = "종합점수"
if score_col not in df.columns:
    raise RuntimeError("CSV에 '종합점수' 컬럼이 없습니다.")
df[score_col] = pd.to_numeric(df[score_col], errors="coerce")

# ========== 2) 이름 정규화 및 _key 심기 ==========
def norm_name(x):
    if pd.isna(x): return None
    s = str(x)
    s = re.sub(r"\s+", "", s)          # 공백 제거
    s = re.sub(r"[(){}\[\]-]", "", s)  # 괄호/하이픈 제거
    s = s.replace("ㆍ", "")
    return s

CSV_KEY = "ADM_DR_NM"  # CSV 동명
GJ_KEY  = "ADM_DR_NM"  # GeoJSON 동명 (파일에 맞게)

if CSV_KEY not in df.columns:
    raise RuntimeError(f"CSV에 '{CSV_KEY}' 컬럼이 없습니다.")
if not gj.get("features"):
    raise RuntimeError("GeoJSON features가 비어 있습니다.")

df["_key"] = df[CSV_KEY].map(norm_name)
for feat in gj["features"]:
    props = feat.get("properties", {}) or {}
    props["_key"] = norm_name(props.get(GJ_KEY))
    feat["properties"] = props

# (선택) 매칭 진단
df_keys = set(df["_key"].dropna().unique())
gj_keys = {feat["properties"].get("_key") for feat in gj["features"] if feat.get("properties")}
print(f"[매칭진단] CSV만 있는 동 수: {len(df_keys - gj_keys)}, GeoJSON만 있는 동 수: {len(gj_keys - df_keys)}")

# ========== 3) 동별 평균 계산 ==========
# hover용 원본 동명 매핑
name_map = (df[[CSV_KEY, "_key"]]
            .dropna()
            .drop_duplicates()
            .groupby("_key")[CSV_KEY]
            .first()
            .reset_index()
            .rename(columns={CSV_KEY: "행정동명"}))

df_mean = (df.dropna(subset=["_key", score_col])
             .groupby("_key", as_index=False)[score_col]
             .mean()
             .rename(columns={score_col: "동별_평균점수"}))

df_mean = df_mean.merge(name_map, on="_key", how="left")

# ========== 4) Q1/Q3 계산 (동별 평균 분포 기준) ==========
Q1 = df_mean["동별_평균점수"].quantile(0.25)
Q3 = df_mean["동별_평균점수"].quantile(0.75)
print(f"[분위수] Q1={Q1:.4f}, Q3={Q3:.4f}")

# 구간 라벨링: Q1 밖(녹), Q1~Q3(연회색), Q3 밖(빨)
df_mean["구간"] = np.select(
    [df_mean["동별_평균점수"] < Q1, df_mean["동별_평균점수"] > Q3],
    ["Q1밖(낮음)", "Q3밖(높음)"],
    default="Q1~Q3"
)

# ========== 5) Choropleth (세 구간 모두 색칠) ==========
color_map = {
    "Q1밖(낮음)": "#2ecc71",  # green
    "Q1~Q3":     "#e0e0e0",  # light gray
    "Q3밖(높음)": "#e74c3c",  # red
}

fig = px.choropleth_mapbox(
    df_mean,                      # 전체(세 구간)
    geojson=gj,
    locations="_key",
    featureidkey="properties._key",
    color="구간",
    color_discrete_map=color_map,
    category_orders={"구간": ["Q1밖(낮음)", "Q1~Q3", "Q3밖(높음)"]},
    hover_name="행정동명",
    hover_data={"동별_평균점수":":.2f", "구간": True},
    mapbox_style="open-street-map",
    opacity=0.88,
    center={"lat": 35.8714, "lon": 128.6014},  # 대구 중심 근처
    zoom=9,
)

fig.update_layout(
    margin=dict(l=0, r=0, t=40, b=0),
    title=f"동별 평균 종합점수 Q1~Q3 포함 색칠 (Q1={Q1:.2f}, Q3={Q3:.2f})",
    legend_title_text="구간"
)

fig.show()
[매칭진단] CSV만 있는 동 수: 0, GeoJSON만 있는 동 수: 0
[분위수] Q1=19.0332, Q3=20.2447

동별 종합점수 평균 지도 시각화

코드
import json, re
import pandas as pd
import plotly.express as px
from pathlib import Path
import numpy as np

# 경로
csv_path = Path("./Data/건축물대장_v0.5.csv")
geojson_path = Path("./Data/시각화/대구_행정동/대구_행정동_군위포함.geojson")

# 1) 데이터 로드
df = pd.read_csv(csv_path)
df.loc[df["ADM_DR_NM"].isna(), "대지위치"]
with open(geojson_path, "r", encoding="utf-8") as f:
    gj = json.load(f)

df["종합점수"] = pd.to_numeric(df["종합점수"], errors="coerce")

# 2) 정규화 함수
def norm_name(x):
    if pd.isna(x): return None
    s = str(x)
    s = re.sub(r"\s+", "", s)
    s = re.sub(r"[(){}\[\]-]", "", s)
    s = s.replace("ㆍ", "")
    return s

# 3) 컬럼
CSV_KEY = "ADM_DR_NM"   # CSV의 행정동명
GJ_KEY  = "ADM_DR_NM"      # GeoJSON의 행정동명 (보통 소문자)

# 4) 키 만들기 (정규화)
df["_key"] = df[CSV_KEY].map(norm_name)
for feat in gj["features"]:
    props = feat.get("properties", {}) or {}
    props["_key"] = norm_name(props.get(GJ_KEY))
    feat["properties"] = props

# 5) 동별 평균 계산
df_avg = (
    df.dropna(subset=["_key"])
      .groupby("_key", as_index=False)["종합점수"]
      .mean()
      .rename(columns={"종합점수": "종합점수_평균"})
)

# 6) 색상 스케일 (하양→빨강)
white_to_red = [
    [0.0, "#ffffff"],
    [1.0, "#ff0000"]
]
vmin = float(df_avg["종합점수_평균"].min())
vmax = float(df_avg["종합점수_평균"].max())

# 7) 시각화
fig = px.choropleth_mapbox(
    df_avg,
    geojson=gj,
    locations="_key",                   # DF의 키
    featureidkey="properties._key",     # GeoJSON의 키
    color="종합점수_평균",
    color_continuous_scale=white_to_red,
    range_color=(vmin, vmax),
    mapbox_style="open-street-map",
    opacity=0.75,
    center={"lat": 35.8714, "lon": 128.6014},
    zoom=9,
    hover_name="_key",
    hover_data={"종합점수_평균":":.2f"}
)
fig.update_layout(margin=dict(l=0,r=0,t=40,b=0), title="동별 점수평균 지도 시각화")
fig.show()